﻿#include "target.h"
#include "context.h"
#include "draw.h"
#include <glad/glad.h>

using namespace std;
using namespace anip;

struct RenderBuffer::Impl
{
	GLuint _hfb = 0;
	Texture2D* _tex = nullptr;
	int _w = -1, _h = -1;
	bool _success = true, _newTex = true;

	stack<Transform2D> trans;
	Transform2D base;
	mat3 mat;

	void use() const
	{
		glBindFramebuffer(GL_FRAMEBUFFER, _hfb) ERROR_CHECKED;
		glViewport(0, 0, _w, _h) ERROR_CHECKED;
	}

	void updateMat()
	{
		if (trans.empty())
			mat = base.toMatrix();
		else
			mat = trans.top().combine(base).toMatrix();
	}

	unsigned char* readData() const
	{
		assert(_w == _tex->width() && _h == _tex->height());

		unsigned char* data = new unsigned char[_w * _h * 4];
		glReadPixels(0, 0, _w, _h, GL_RGBA, GL_UNSIGNED_BYTE, data) ERROR_CHECKED;
		return data;
	}

	unsigned char* readData(int x, int y, int w, int h) const
	{
		assert(_w == _tex->width() && _h == _tex->height());
		assert(x >= 0 && y >= 0);
		assert(w <= _w && h <= _h);

		unsigned char* data = new unsigned char[w * h * 4];
		glReadPixels(x, y, w, h, GL_RGBA, GL_UNSIGNED_BYTE, data) ERROR_CHECKED;
		return data;
	}

	void readDataTo(unsigned char* data) const
	{
		glReadPixels(0, 0, _w, _h, GL_RGBA, GL_UNSIGNED_BYTE, data) ERROR_CHECKED;
	}

	void readDataTo(int x, int y, int w, int h, unsigned char* data) const
	{
		assert(w <= _w && h <= _h);
		glReadPixels(x, y, w, h, GL_RGBA, GL_UNSIGNED_BYTE, data) ERROR_CHECKED;
	}

	Impl();
	Impl(int w, int h, int samples, const TextureOptions& options);
	Impl(Texture2D& tex);
};

RenderBuffer::Impl::Impl() {}

RenderBuffer::Impl::Impl(int w, int h, int samples, const TextureOptions& options)
{
	glGenFramebuffers(1, &_hfb) ERROR_CHECKED;
	glBindFramebuffer(GL_FRAMEBUFFER, _hfb) ERROR_CHECKED;

	if (samples > 0)
		_tex = Texture2D::createMultisample(w, h, samples);
	else
		_tex = new Texture2D(w, h, &options);

	glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
		_tex->isMultisample() ? GL_TEXTURE_2D_MULTISAMPLE : GL_TEXTURE_2D,
		_tex->id(), 0) ERROR_CHECKED;
	GLenum draws[1] = { GL_COLOR_ATTACHMENT0 };
	glDrawBuffers(1, draws) ERROR_CHECKED;

	GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
	if (status != GL_FRAMEBUFFER_COMPLETE)
	{
		_success = false;
		throw runtime_error("Failed to create RenderBuffer");
	}
	glBindFramebuffer(GL_FRAMEBUFFER, 0) ERROR_CHECKED;
}

RenderBuffer::Impl::Impl(Texture2D& tex)
{
	glGenFramebuffers(1, &_hfb) ERROR_CHECKED;
	glBindFramebuffer(GL_FRAMEBUFFER, _hfb) ERROR_CHECKED;

	_tex = &tex;
	_newTex = false;

	glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
		_tex->isMultisample() ? GL_TEXTURE_2D_MULTISAMPLE : GL_TEXTURE_2D,
		_tex->id(), 0) ERROR_CHECKED;
	GLenum draws[1] = { GL_COLOR_ATTACHMENT0 };
	glDrawBuffers(1, draws) ERROR_CHECKED;

	GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
	if (status != GL_FRAMEBUFFER_COMPLETE)
	{
		_success = false;
		throw runtime_error("Failed to create RenderBuffer");
	}
	glBindFramebuffer(GL_FRAMEBUFFER, 0) ERROR_CHECKED;
}

RenderBuffer::RenderBuffer() :
	_pimpl(new Impl()) 
{}

RenderBuffer::RenderBuffer(int w, int h, int samples):
	_pimpl(new Impl(w, h, samples, TextureOptions()))
{ 
	_setSize(w, h);
}

RenderBuffer::RenderBuffer(int w, int h, const TextureOptions& options):
	_pimpl(new Impl(w, h, 0, options)) 
{
	_setSize(w, h);
}

RenderBuffer::RenderBuffer(Texture2D& tex):
	_pimpl(new Impl(tex)) 
{
	_setSize(tex.width(), tex.height());
}

RenderBuffer::~RenderBuffer() = default;

bool RenderBuffer::success() const { return _pimpl->_success; }
unsigned int RenderBuffer::id() const { return _pimpl->_hfb; }
const Texture2D& RenderBuffer::getTexture() const { return *_pimpl->_tex; }

int RenderBuffer::width() const { return _pimpl->_w; }
int RenderBuffer::height() const { return _pimpl->_h; }

vec2 RenderBuffer::size() const { return { _pimpl->_w, _pimpl->_h }; }

void anip::RenderBuffer::_setSize(int w, int h)
{
	_pimpl->_w = w;
	_pimpl->_h = h;
	_pimpl->base = Transform2D({ -1, 1 }, vec2{ 2.0f / w, -2.0f / h });
	_pimpl->updateMat();
}

const Transform2D &anip::RenderBuffer::_baseTransform() const
{
	return _pimpl->base;
}

mat3 anip::RenderBuffer::transformMatrix() const
{
	return _pimpl->mat;
}

Transform2D RenderBuffer::transform() const
{
	return _pimpl->trans.empty() ? Transform2D() : _pimpl->trans.top();
}

void RenderBuffer::transformReset()
{
	while (!_pimpl->trans.empty())
		_pimpl->trans.pop();
	_pimpl->updateMat();
}

void RenderBuffer::transformPush(const Transform2D& t)
{
	_pimpl->trans.push(t);
	_pimpl->updateMat();
}

void RenderBuffer::transformPushCombined(const Transform2D& t)
{
	if (_pimpl->trans.empty())
		transformPush(t);
	else
		transformPush(t.combine(transform()));
}

void RenderBuffer::transformPop()
{
	assert(!_pimpl->trans.empty());
	_pimpl->trans.pop();
	_pimpl->updateMat();
}

void RenderBuffer::beginRender()
{
	Context::_pushBuffer(*this);
	_pimpl->use();
}

void RenderBuffer::endRender() const
{
	auto b = Context::_popBuffer();
	assert(b == this);
	Context::currentTarget()._pimpl->use();
}

void RenderBuffer::blitTo(const RenderBuffer& other) const
{
	glBindFramebuffer(GL_READ_FRAMEBUFFER, _pimpl->_hfb) ERROR_CHECKED;
	glBindFramebuffer(GL_DRAW_FRAMEBUFFER, other._pimpl->_hfb) ERROR_CHECKED;
	glBlitFramebuffer(
		0, 0, _pimpl->_w, _pimpl->_h, 
		0, 0, _pimpl->_w, _pimpl->_h, 
		GL_COLOR_BUFFER_BIT, GL_NEAREST) ERROR_CHECKED;
}

void RenderBuffer::blitTo(const RenderBuffer& other,
	int x1, int y1, int w1, int h1,
	int x2, int y2, int w2, int h2, bool filter) const
{
	assert(x1 >= 0 && x2 >= 0 && y1 >= 0 && y2 >= 0);

	glBindFramebuffer(GL_READ_FRAMEBUFFER, _pimpl->_hfb) ERROR_CHECKED;
	glBindFramebuffer(GL_DRAW_FRAMEBUFFER, other._pimpl->_hfb) ERROR_CHECKED;
	glBlitFramebuffer(x1, y1, x1 + w1, y1 + h1, x2, y2, x2 + w2, x2 + h2, 
		GL_COLOR_BUFFER_BIT, filter ? GL_LINEAR : GL_NEAREST) ERROR_CHECKED;
}

void RenderBuffer::copyTo(const Texture2D& tex) const
{
	assert(!tex.isMultisample());

	glBindFramebuffer(GL_READ_FRAMEBUFFER, _pimpl->_hfb) ERROR_CHECKED;
	tex.use();
	glCopyTexImage2D(GL_TEXTURE_2D, 0, (GLenum)tex.format(), 
		0, 0, tex.width(), tex.height(), 0) ERROR_CHECKED;
}

void RenderBuffer::copyTo(const Texture2D& tex, 
	int x1, int y1, int x2, int y2, int w, int h) const
{
	assert(!tex.isMultisample());
	assert(x1 >= 0 && x2 >= 0 && y1 >= 0 && y2 >= 0);

	w = min(w, tex.width() - x2);
	h = min(h, tex.height() - y2);

	glBindFramebuffer(GL_READ_FRAMEBUFFER, _pimpl->_hfb) ERROR_CHECKED;
	tex.use(0);
	glCopyTexSubImage2D(GL_TEXTURE_2D, 0, x2, y2, x1, y1, w, h) ERROR_CHECKED;
}

unsigned char* RenderBuffer::readData() const
{
	return _pimpl->readData();
}

unsigned char* RenderBuffer::readData(int x, int y, int w, int h) const
{
	return _pimpl->readData(x, y, w, h);
}

void RenderBuffer::readDataTo(unsigned char* data) const
{
	_pimpl->readDataTo(data);
}

void RenderBuffer::readDataTo(int x, int y, int w, int h, unsigned char* data) const
{
	_pimpl->readDataTo(x, y, w, h, data);
}

void RenderBuffer::finalize()
{
	if (_pimpl->_newTex)
	{
		_pimpl->_tex->finalize();
		delete _pimpl->_tex;
	}

	glDeleteFramebuffers(1, &_pimpl->_hfb) ERROR_CHECKED;
	glBindFramebuffer(GL_FRAMEBUFFER, 0) ERROR_CHECKED;
}